/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.meego.common.stats;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLongArray;

/**
 *
 * @author TueHM
 */
public class EstimatedHistogram {

    private long[] bucketOffsets;
    final AtomicLongArray buckets;

    public EstimatedHistogram() {
        this(90);
    }

    public EstimatedHistogram(int bucketCount) {
        makeOffsets(bucketCount);
        this.buckets = new AtomicLongArray(this.bucketOffsets.length + 1);
    }

    public EstimatedHistogram(long[] offsets, long[] bucketData) {
        assert (bucketData.length == offsets.length + 1);
        this.bucketOffsets = offsets;
        this.buckets = new AtomicLongArray(bucketData);
    }

    private void makeOffsets(int size) {
        this.bucketOffsets = new long[size];
        long last = 1L;
        this.bucketOffsets[0] = last;
        for (int i = 1; i < size; i++) {
            long next = Math.round(last * 1.2D);
            if (next == last) {
                next += 1L;
            }
            this.bucketOffsets[i] = next;
            last = next;
        }
    }

    public long[] getBucketOffsets() {
        return this.bucketOffsets;
    }

    public void add(long n) {
        int index = Arrays.binarySearch(this.bucketOffsets, n);
        if (index < 0) {
            index = -index - 1;
        }

        this.buckets.incrementAndGet(index);
    }

    long get(int bucket) {
        return this.buckets.get(bucket);
    }

    public long[] getBuckets(boolean reset) {
        long[] rv = new long[this.buckets.length()];
        for (int i = 0; i < this.buckets.length(); i++) {
            rv[i] = this.buckets.get(i);
        }
        if (reset) {
            for (int i = 0; i < this.buckets.length(); i++) {
                this.buckets.set(i, 0L);
            }
        }
        return rv;
    }

    public long min() {
        for (int i = 0; i < this.buckets.length(); i++) {
            if (this.buckets.get(i) > 0L) {
                return i == 0 ? 0L : 1L + this.bucketOffsets[(i - 1)];
            }
        }
        return 0L;
    }

    public long max() {
        int lastBucket = this.buckets.length() - 1;
        if (this.buckets.get(lastBucket) > 0L) {
            return 9223372036854775807L;
        }
        for (int i = lastBucket - 1; i >= 0; i--) {
            if (this.buckets.get(i) > 0L) {
                return this.bucketOffsets[i];
            }
        }
        return 0L;
    }

    public long mean() {
        int lastBucket = this.buckets.length() - 1;
        if (this.buckets.get(lastBucket) > 0L) {
            throw new IllegalStateException("Unable to compute ceiling for max when histogram overflowed");
        }
        long elements = 0L;
        long sum = 0L;
        for (int i = 0; i < lastBucket; i++) {
            elements += this.buckets.get(i);
            sum += this.buckets.get(i) * this.bucketOffsets[i];
        }

        return (long)Math.ceil(sum / elements);
    }

    public long count() {
        long sum = 0L;
        for (int i = 0; i < this.buckets.length(); i++) {
            sum += this.buckets.get(i);
        }
        return sum;
    }

    public boolean isOverflowed() {
        return this.buckets.get(this.buckets.length() - 1) > 0L;
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof EstimatedHistogram)) {
            return false;
        }
        EstimatedHistogram that = (EstimatedHistogram) o;
        return (Arrays.equals(getBucketOffsets(), that.getBucketOffsets())) && (Arrays.equals(getBuckets(false), that.getBuckets(false)));
    }
}